package com.tsing.cli.system.config;

import com.tsing.cli.common.annotation.AnonymousAccess;
import com.tsing.cli.common.config.RedisManager;
import com.tsing.cli.system.filter.TokenFilter;
import com.tsing.cli.system.service.impl.AccessDeniedHandlerImpl;
import com.tsing.cli.system.service.impl.AuthenticationEntryPointImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.*;

/**
 * Security配置
 *
 * @author TheTsing
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Value("${login.token-expire:7200}")
    private Long tokenExpire;

    private final RedisManager redisManager;

    private final ApplicationContext applicationContext;

    /**
     * 密码加密和验证策略
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 手动注入AuthenticationManager
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * HttpSecurity配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
        Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);
        return httpSecurity
                .cors()
                .and()
                // csrf禁用
                .csrf().disable()
                // 取消限制iframe引用页面
                .headers().frameOptions().disable()
                .and()
                .exceptionHandling()
                // 认证异常处理器
                .authenticationEntryPoint(new AuthenticationEntryPointImpl())
                // 授权异常处理器
                .accessDeniedHandler(new AccessDeniedHandlerImpl())
                .and()
                // 关闭session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
                        // 放行特殊请求
                        .requestMatchers("/websocket/**", "/error", "/druid/**", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll()
                        // 放行OPTIONS请求
                        .requestMatchers(HttpMethod.OPTIONS).permitAll()
                        // 放行自定义匿名访问的url
                        .requestMatchers(HttpMethod.GET, anonymousUrls.get("GET").toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.POST, anonymousUrls.get("POST").toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.PUT, anonymousUrls.get("PUT").toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.PATCH, anonymousUrls.get("PATCH").toArray(new String[0])).permitAll()
                        .requestMatchers(HttpMethod.DELETE, anonymousUrls.get("DELETE").toArray(new String[0])).permitAll()
                        .requestMatchers(anonymousUrls.get("ALL").toArray(new String[0])).permitAll()
                        // 其他请求都需要认证
                        .anyRequest().authenticated())
                .addFilterBefore(new TokenFilter(tokenExpire, redisManager), UsernamePasswordAuthenticationFilter.class)
                .build();
    }

    private Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
        Map<String, Set<String>> anonymousUrls = new HashMap<>(8);
        Set<String> get = new HashSet<>();
        Set<String> post = new HashSet<>();
        Set<String> put = new HashSet<>();
        Set<String> patch = new HashSet<>();
        Set<String> delete = new HashSet<>();
        Set<String> all = new HashSet<>();
        handlerMethodMap.forEach((k, v) -> {
            AnonymousAccess anonymousAccess = v.getMethodAnnotation(AnonymousAccess.class);
            if (Objects.nonNull(anonymousAccess)) {
                List<RequestMethod> requestMethods = new ArrayList<>(k.getMethodsCondition().getMethods());
                if (requestMethods.isEmpty()) {
                    all.addAll(k.getPathPatternsCondition().getPatternValues());
                } else {
                    switch (requestMethods.get(0)) {
                        case GET -> get.addAll(k.getPathPatternsCondition().getPatternValues());
                        case POST -> post.addAll(k.getPathPatternsCondition().getPatternValues());
                        case PUT -> put.addAll(k.getPathPatternsCondition().getPatternValues());
                        case PATCH -> patch.addAll(k.getPathPatternsCondition().getPatternValues());
                        case DELETE -> delete.addAll(k.getPathPatternsCondition().getPatternValues());
                        default -> all.addAll(k.getPathPatternsCondition().getPatternValues());
                    }
                }
            }
        });
        anonymousUrls.put("GET", get);
        anonymousUrls.put("POST", post);
        anonymousUrls.put("PUT", put);
        anonymousUrls.put("PATCH", patch);
        anonymousUrls.put("DELETE", delete);
        anonymousUrls.put("ALL", all);
        return anonymousUrls;
    }

}
